/**
 * 19.11.28 增加resetAll()中的排除项
 * 19.12.9 规范化
 * 20221014:restructuring with typescript, major alteration.
*/

import { Color, Matrix3, Matrix4, Quaternion, Vector2, Vector3, Vector4 } from "three";
import {Utils,BaseType, Loose} from "./utils";

type allow=number|
    Array<number|boolean|string|Color|Vector2|Vector3|Vector4|Matrix3|Matrix4|Quaternion|
        Array<number|boolean|string|Color|Vector2|Vector3|Vector4|Matrix3|Matrix4|Quaternion|
            Array<number|boolean|string|Color|Vector2|Vector3|Vector4|Matrix3|Matrix4|Quaternion>>>|
    boolean|string|Color|Vector2|Vector3|Vector4|Matrix3|Matrix4|Quaternion;

interface CallbackHandle<T>{
    (newValue:T,oldValue:T):void
}

/**
 * property, only for base value type in allowTyps
 */
class Prop<T>{
    private _value!:T;
    private _type:string;
    public readonly key:string;
    public oldValue:T;
    public oriValue:T;
    public alwaysCallback:boolean;
    public callback:CallbackHandle<T>|null=null;
    public readonly type='Prop';
    constructor(key:string,value:T){
        this._type=Utils.typeOf(value);
        this.key=key;
        this.value=value;
        this.oldValue=value;
        this.oriValue=value;
        this.alwaysCallback=false;//是否总是执行回调
    }

    /**
     * destroy
     */
    public dispose():void{
        this.callback=null;
    }

    /**
     * get old value
     */
    public get old(){
        return this.oldValue;
    }

    /**
     * get current value
     */
    public get value(){
        return this._value;
    }

    /**
     * set value
     */
    public set value(val:T){
        if(!this.alwaysCallback) {
            if(this.oldValue!==undefined){
                if (Utils.equal(this.oldValue, val,this._type)){
                    return;
                }
            }
        }
        const n=val;
        const o=this.oldValue;
        this._value=val;
        this.oldValue=val;
        if(this.callback)this.callback(n,o);
    }

    /**
     * reset to originalvalue
     */
    public reset(call:boolean=false):void{
        let n=this.oriValue;
        let o=this._value;
        this._value = this.oriValue;
        this.oldValue=this.oriValue;
        if(!call)return;
        if(this.callback)this.callback(n,o);
    }

    /**
     * set callback handler
     */
    public onChange(callback:CallbackHandle<T>|null):Prop<T>{
        this.callback=callback;
        return this;
    }

}

/**
 * Attribute set (no reference related processing is done inside the attribute assignment, but it is handled externally)
 * example: let _props=new props({a:0,b:true,c:null});
 */
class Props<T>{

    private _finalKey:string|null;
    private _callbacksMap:Loose<CallbackHandle<any>|null>={};
    private _props:Array<Prop<any>>=[];
    private _map:Loose<Prop<any>>={};
    private _onChangeCallback:{():void}|null=null;
    
    public readonly type:string='Props';

    constructor(...props:Array<Prop<allow>>|Array<Loose<allow>>){
        let ps=Utils.typeOf(props[0]) === 'Prop'?props:this._generatePropsFromObject(props as Array<Loose<any>>);
        this._finalKey=null;
        this._props.push(...ps as Array<Prop<any>>);
        this._init(ps as Array<Prop<any>>);
    }

    public get map():T{
        return this._map as T;
    }
    
    public prop(key:string):Prop<any>|null{
        const prop=this._map[key];
        if(!prop)return null;
        return prop;
    }
    
    public value(key:string, value?:any):any{
        const prop=this._map[key];
        if(!prop)return null;
        if(value!==undefined){
            prop.value=value;
        }
        return prop.value;
    }

    /**
     * Set the execution key (when multiple attributes that will cause the same operation are changed at the same time,
     * the operation will be executed only when the execution key is changed)
     * When multiple attributes need to be changed frequently at the same time, executing the key can greatly improve performance
     */
    public setFinalKey(key:string):void{
        this._finalKey=key;
    }

    /**
     * Reset the execution key (leave the execution key blank, otherwise the callback function will be executed only 
     * when the execution key changes)
     */
    public resetFinalKey():void{
        this._finalKey=null;
    }

    /**
     * add properties
     */
    public add(...props:Array<Prop<any>>|Array<Loose<any>>):void{
        let ps=Utils.typeOf(props) === BaseType.ARRAY?props:this._generatePropsFromObject(props as Array<Loose<any>>);
        this._props.push(...ps as Array<Prop<any>>)
        this._setKeys(ps as Array<Prop<any>>);
    }

    /**
     * remove properties
     */
    public remove(...keys:Array<string>):void{
        for(let key of keys) {
            const prop=this._map[key];
            if(!prop)continue;
            this._callbacksMap[key] = null;
            const idx=Utils.inElements(prop,this._props,(a,b)=>{return a.key===b.key},true) as number;
            this._props.splice(idx,1);
            prop.dispose();
            delete this._map[key];
        }
    }

    /**
     * get all properties
     */
    public all(onlyValue:boolean=false):Loose<any|Prop<any>>{
        const out:Loose<any|Prop<any>>={};
        for(let prop of this._props){
            const key=prop.key;
            if(onlyValue){
                out[key]=this._map[key].value!;
            }else {
                out[key] = this._map[key];
            }
        }
        return out;
    }

    /**
     * destroy
     */
    public dispose():void{
        for(let prop of this._props){
            prop.dispose();
        }
    }

    /**
     * Reset all elements to the original value (whether to execute callback, exclusive key)
     */
    public resetAll(call:boolean=false,...exceptions:Array<string>):void{
        for(let prop of this._props){
            if(exceptions.indexOf(prop.key)!==-1)continue;
            prop.reset(call);
        }
    }

    /**
     * Execute callback when a property changes
     */
    public onKeyChange(key:string,callback:CallbackHandle<any>):Props<T>{
        if(this._callbacksMap[key])this._callbacksMap[key]=callback;
        return this;
    }

    /**
     * Execute callback when any property changes
     */
    public onChange(callback:{():void}):Props<T>{
        this._onChangeCallback=callback;
        return this;
    }

    /**
     * Execute the same callback when multiple attributes change
     */
    public onMultiKeyChange(callback:CallbackHandle<any>,...keys:Array<string>){
        keys.forEach((key)=>{
            if(this._callbacksMap[key])this._callbacksMap[key]=callback;
        });
    }

    //----------------------------------------私有属性及方法------------------------------------------

    private _init(props:Array<Prop<any>>):void{
        this._callbacksMap={};
        this._setKeys(props);
    }
    private _setKeys(props:Array<Prop<any>>):void{
        for(let prop of props){
            let key=prop.key;
            if(this._map[key]){
                console.warn('this property is already exist:',key);
                continue;
            }
            this._map[key]=prop;
            prop.onChange((n,o)=>{
                if(this._finalKey!==key&&this._finalKey!==undefined&&this._finalKey!==null)return;
                if(this._onChangeCallback)this._onChangeCallback();
                if(this._callbacksMap[key])this._callbacksMap[key]!(n,o);
            });
        }
    }
    private _generatePropsFromObject(objs:Array<Loose<any>>):Array<Prop<any>>{
        const ps:Array<Prop<any>>=[];
        for(let obj of objs){
            for(let key in obj){
                const val= obj[key];
                ps.push(new Prop(key, val));
            }
        }
        return ps;
    }
}

export {Props,Prop};